﻿/*
 * LINQ to Active Directory
 * http://www.codeplex.com/LINQtoAD
 * 
 * Copyright Bart De Smet (C) 2007
 * info@bartdesmet.net - http://blogs.bartdesmet.net/bart
 * 
 * This project is subject to licensing restrictions. Visit http://www.codeplex.com/LINQtoAD/Project/License.aspx for more information.
 */

#region Namespace imports

using System.ComponentModel;
using System.Reflection;
using System;
using System.Linq;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Collections.Generic;
using Novell.Directory.Ldap;
#endregion

namespace Novell.DirectoryServices.Linq
{
    /// <summary>
    /// Represents an entity in Directory Services.
    /// </summary>
    public abstract class DirectoryEntity : INotifyPropertyChanged
    {
        /// <summary>
        /// Wrapped DirectoryEntry.
        /// </summary>
        private LdapEntry directoryEntry;
        private WeakReference _directorySource;

        private string[] _objectClasses;
        private PropertyInfo _defaultNamingProperty;
        private string _defaultNamingAttribute;

#if ENABLE_REQUIRED_ATTRIBUTES
        System.Collections.Generic.Dictionary<string,List<string>> _requiredAttributes;
        internal System.Collections.Generic.Dictionary<string, List<string>> RequiredAttributes
        {
            get
            {
                if (_requiredAttributes == null)
                    InitializeSchemaReflection();
                return _requiredAttributes;
            }
        }
#endif
        internal string PrimarySchemaClassName
        {
            get
            {
                if (_objectClasses == null)
                    InitializeSchemaReflection();
                return _objectClasses.Count() == 0 ? null : _objectClasses[0];
            }
        }
        internal string[] ObjectClasses
        {
            get
            {
                if (_objectClasses == null)
                    InitializeSchemaReflection();
                return _objectClasses;
            }
        }
        internal string DefaultNamingAttributeValue
        {
            get
            {
                if (_defaultNamingAttribute == null)
                    InitializeAttributeReflection();
                if (_defaultNamingProperty != null)
                {
                    object o = _defaultNamingProperty.GetValue(this, null);
                    if (o != null)
                        return o.ToString();
                }
                return null;
            }
        }
        internal string DefaultNamingAttribute
        {
            get
            {
                if (_defaultNamingAttribute == null)
                    InitializeAttributeReflection();
                return _defaultNamingAttribute;
            }
        }
        private void InitializeAttributeReflection()
        {
            Type t = this.GetType();
            foreach (PropertyInfo prop in t.GetProperties())
            {
                DirectoryAttributeAttribute[] da = prop.GetCustomAttributes(typeof(DirectoryAttributeAttribute), false) as DirectoryAttributeAttribute[];
                if (da == null) return;
                foreach (DirectoryAttributeAttribute attribute in da)
                {
                    if (attribute.DefaultNamingAttribute)
                    {
                        //This is only needed if we do more in the loop, we know that this will only be run once and can stop
                        //so this check will always be false.
                        if (_defaultNamingProperty != null)
                        {
                            throw new InvalidOperationException("Only one attribute can be marked with DefaultNamingAttribute = true.");
                        }

                        this._defaultNamingAttribute = attribute.Attribute;
                        this._defaultNamingProperty = prop;
                        //If we do more with this loop we might not have the break in here but we know we can stop once this is true.
                        //break; I am commenting this out so we can detect more than one attribute set to true
                        //this is so that the programmer can detect a definition error easier.
                    }
                }
            }
        }
        private void InitializeSchemaReflection()
        {
            Type t = this.GetType();
            DirectorySchemaAttribute[] schemaAttributes = (DirectorySchemaAttribute[])t.GetCustomAttributes(typeof(DirectorySchemaAttribute), true);
            _objectClasses = new string[schemaAttributes.Count()];
            //_requiredAttributes = new Dictionary<string, List<string>>();
            int i = 1;
            if (schemaAttributes == null) return;
            foreach (DirectorySchemaAttribute schemaAttribute in schemaAttributes)
            {
                //Get the primary schema object and put it first
                if (schemaAttribute.Primary)
                {
                    _objectClasses[0] = schemaAttribute.Schema;
                }
                else
                {
                    _objectClasses[i] = schemaAttribute.Schema;
                    i++;
                }
                //TODO: Completely implement the source of retrieving the required attributes
#if ENABLE_REQUIRED_ATTRIBUTES
                //if (schemaAttribute.RequiredAttributes != null)
                //{
                //    _requiredAttributes.Add(schemaAttribute.Schema, new List<string>(schemaAttribute.RequiredAttributes));
                //}
#endif
            }
        }
        internal void InitializeInternal(IDirectorySource source, LdapEntry entry)
        {
            //Hold onto a copy of the IDirectorySource instance.
            _directorySource = new WeakReference(source);
            //Keep a copy of the directory entry.
            //TODO: Implement IDisposable.
            directoryEntry = entry;
            //Initialize any child source objects
            if (directoryEntry != null)
            {
                InitializeChildSources();
            }
        }
        internal void InitializeInternal(IUpdatableDirectorySource source, LdapEntry entry)
        {
            source.RegisterForSubmissionNotifications(this);
            //Add a handler for the property changed event.
            PropertyChanged += new PropertyChangedEventHandler(source.UpdateNotification);

            InitializeInternal((IDirectorySource)source, entry);
        }
        /// <summary>
        /// Gets the DirectorySource for the given entity type.
        /// </summary>
        /// <typeparam name="T">Entity type.</typeparam>
        /// <param name="scope">Search scope.</param>
        /// <returns>DirectorySource for the given entity type.</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter")]
        public DirectorySource<T> GetSource<T>(string root, SearchScope scope)
        {
            SearchOptions o = new SearchOptions(root, scope);
            return new DirectorySource<T>(this.DirectorySource.Context,o);
        }
        /// <summary>
        /// Gets an UpdatableDirectorySource for the given entity type.
        /// </summary>
        /// <typeparam name="T">Entity type.</typeparam>
        /// <param name="scope">Search scope.</param>
        /// <returns>DirectorySource for the given entity type.</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter")]
        public UpdatableDirectorySource<T> GetUpdatableSource<T>(string root,SearchScope scope) where T : DirectoryEntity
        {
            SearchOptions o = new SearchOptions(root, scope);
            return new UpdatableDirectorySource<T>(this.DirectorySource.Context, o);
        }
        private void InitializeChildSources()
        {

            MethodInfo getSource = typeof(DirectoryEntity).GetMethod("GetSource");
            MethodInfo getUpdatableSource = typeof(DirectoryEntity).GetMethod("GetUpdatableSource");
            
            foreach (PropertyInfo property in this.GetType().GetProperties())
            {
                //DirectoryEntry searchRoot = null;
                if (property.PropertyType.IsGenericType && property.PropertyType.GetGenericTypeDefinition() == typeof(UpdatableDirectorySource<>))
                {
                    InitializeChildSource(getUpdatableSource, property);
                }
                else if (property.PropertyType.IsGenericType && property.PropertyType.GetGenericTypeDefinition() == typeof(DirectorySource<>))
                {
                    InitializeChildSource(getSource, property);
                }
            }
        }
        private void InitializeChildSource(MethodInfo getSourceMethod, PropertyInfo property)
        {
            var searchOptions = property.GetCustomAttributes(typeof(DirectorySearchOptionsAttribute), true).Cast<DirectorySearchOptionsAttribute>().FirstOrDefault();
            SearchScope scope = searchOptions != null ? searchOptions.Scope : SearchScope.Base;
            string searchRoot = GetSearchRootForProperty(property);

            Type entity = property.PropertyType.GetGenericArguments()[0];
            MethodInfo mi = getSourceMethod.MakeGenericMethod(entity);
            object o = mi.Invoke(this, new object[] { searchRoot, scope });
            property.SetValue(this, o, null);
        }
        private string GetSearchRootForProperty(PropertyInfo property)
        {
            string searchRoot = this.directoryEntry.DN;

            Debug.Assert(this.directoryEntry != null, "InitializeChildSources was called with DirectoryEntry not initialized");
            
            var searchPath = property.GetCustomAttributes(typeof(DirectorySearchPathAttribute), true).Cast<DirectorySearchPathAttribute>().FirstOrDefault();
            if (searchPath != null && !string.IsNullOrEmpty(searchPath.Path))
            {
                try
                {
                    Debug.Assert(this.DirectorySource != null, "InitializeChildSources was called with DirectorySource not initilaized");
                    searchRoot = searchPath.Path + "," + searchRoot;
#if ENSURECHILDSEARCHROOTSEXIST || DEBUG
                    //TODO: We must do this for entries that do not exist.
                    //Test if the entry exists or not.

                    string pQuery = "(" + searchPath.Path + ")";
                    //We have to use the async method here.
                    //For some reason the sync method has a timing issue.  Multithreading problem maybe?
                    LdapSearchQueue res = this.DirectorySource.Context.Connection.Search(this.directoryEntry.DN
                                                                    , LdapConnection.SCOPE_ONE
                                                                    , pQuery
                                                                    , null
                                                                    , false
                                                                    ,(LdapSearchQueue)null
                                                                    ,(LdapSearchConstraints)null);
                    bool b = false;
                    LdapMessage message;
                    while ((message = res.getResponse()) != null)
                    {
                        if (message.Type == 4)//LdapMessage.SEARCH_RESULT The Enumeration specifies the wrong value 4=5 and 5=4
                        {
                            b = true;
                            break;
                        }
                    }
                    if (b == false)
                    {
                        LdapAttributeSet set = new LdapAttributeSet();
                        string[] sp = searchPath.Path.Split('=');
                        set.Add(new LdapAttribute("objectClass", "organizationalUnit"));
                        set.Add(new LdapAttribute(sp[0], sp[1]));
                        LdapEntry newEntry = new LdapEntry(searchRoot, set);
                        this.DirectorySource.Context.Connection.Add(newEntry);
                    }
#endif
                }
                catch (LdapException ex)
                {
                    throw new InvalidOperationException("Failed to retrieve nested context " + property.Name + " with search path " + searchPath.Path + ".", ex);
                }
            }

            return searchRoot;
        }

        /// <summary
        /// Gets or sets the directory source that this entity came from.
        /// The source also has access to the context.
        /// </summary>
        internal IDirectorySource DirectorySource
        {
            get { return (_directorySource != null && _directorySource.IsAlive) ? (IDirectorySource)(_directorySource.Target) : null; }
            //set { _directorySource = new WeakReference(value); }
        }
        /// <summary>
        /// Gets/sets the underlying DirectoryEntry.
        /// </summary>
        protected internal LdapEntry DirectoryEntry
        {
            get { return directoryEntry; }
            //set { directoryEntry = value; }
        }
        /// <summary>
        /// Overridden in the entity 
        /// </summary>
        public bool IsNew
        {
            get
            {
                return directoryEntry == null || string.IsNullOrEmpty(directoryEntry.DN);
            }
        }
        /// <summary>
        /// Event raised when a property on a subclass (= strongly typed entity) has been changed.
        /// This is used for change tracking.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// Raises the PropertyChanged event for the specified entity property.
        /// </summary>
        /// <param name="propertyName">Entity property that has been changed.</param>
        protected void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
        //This class needs to be notified when it is updated so that it can send the update 
        /// <summary>
        /// Called by the framework (DirectorySource) when updating it.
        /// </summary>
        internal void SubmitChangesInternal()
        {
            foreach (PropertyInfo property in this.GetType().GetProperties())
            {
                if (property.PropertyType.IsGenericType && property.PropertyType.GetGenericTypeDefinition() == typeof(UpdatableDirectorySource<>))
                {
                    //Call update on the property
                    IUpdatableDirectorySource source = property.GetValue(this, null) as IUpdatableDirectorySource;
                    source.SubmitChanges();
                }
            }
        }    
    }
}